package com.google.appengine;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import com.google.appengine.repackaged.com.google.common.base.Joiner;

public class DevAppServerController implements AutoCloseable {

  public static final byte PING    = 0; // is there a controller running?
  public static final byte SERVING = 1; // is there a development server running for a given id?
  public static final byte START   = 2; // start server for id
  public static final byte STOP    = 3; // stop server for id
  public static final byte KILL    = 5; // kill the controller
  public static final byte BYE     = 6; // close the connnection
  public static final byte LOG     = 7; // set up log streams
  
  public static final byte OK = 0;
  public static final byte ERR = 1;
  public static final byte STDOUT = 2;
  public static final byte STDERR = 3;
  
  private final DevAppServerPreferences prefs;
  private final Map<Long, Process> processes = new HashMap<Long, Process>();
  private final Map<Long, Thread> outThreads = new HashMap<Long, Thread>();
  private final Map<Long, Thread> errThreads = new HashMap<Long, Thread>();
  
  private boolean killed = false;
  
  private DevAppServerController(DevAppServerPreferences prefs) {
    this.prefs = prefs;
  }
  
  @Override
  public synchronized void close() {
    for (Thread thread : errThreads.values()) {
      thread.interrupt();
    }
    errThreads.clear();
    
    for (Thread thread : outThreads.values()) {
      thread.interrupt();
    }
    outThreads.clear();
    
    for (Process process : processes.values()) {
      process.destroy();
    }
    processes.clear();
  }
  
  private void run() {
    try (ServerSocket serverSocket = new ServerSocket()) {
      serverSocket.bind(null);
      System.out.println("Dev App Server Controller started on port " + serverSocket.getLocalPort());
      
      prefs.setPort(serverSocket.getLocalPort());      
      
      while (!killed) {
        final Socket socket = serverSocket.accept();
        Thread listener = new Thread() {
          @Override
          public void run() {
            handleConnection(socket);
          }
        };
        listener.start();
      }
    } catch (Exception e) {
      System.err.println("Dev App Server Error: " + e.getMessage());
    } finally {
      close();
    }
  }

  private void handleConnection(Socket socket) {
    System.out.println("Dev App Server Controller got connection from " + socket.getInetAddress());
    try (DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream())) {
      
      long id;
      boolean synchronous;
      Process process;
      List<String> serverCommand;
      
      while (!Thread.interrupted()) {
        byte command = input.readByte();
        switch (command) {
        case PING:
          System.out.println("Dev App Server got ping");
          output.writeByte(OK);
          break;
        case SERVING:
          id = input.readLong();
          boolean alive = false;
          synchronized (this) {
            process = processes.get(id);
            alive = process != null && isAlive(process);
          }
          System.out.println("Dev App Server Controller serving " + id + ": " + alive);
          output.writeByte(alive ? OK : ERR);
          break;
        case START:
          id = input.readLong();
          synchronous = input.readBoolean();
          int num = input.readInt();
          serverCommand = new ArrayList<String>();
          for (int i = 0; i < num; ++i) {
            String line = input.readUTF();
            serverCommand.add(line);
          }
          
          System.out.println("Dev App Server Controller starting app server " + id + " " + (synchronous ? "synchronous" : "asynchronous") + Joiner.on(" ").join(serverCommand));          
          try {
            start(id, serverCommand, synchronous);
            output.writeByte(OK);
          } catch (Exception e) {
            output.writeByte(ERR);
            output.writeUTF(e.getMessage());
          }
          break;
        case LOG:
          id = input.readLong();
          System.out.println("Dev App Server Controller initialize logging for " + id);
          setupLogging(id, socket, output);
          break;
        case STOP:
          id = input.readLong();
          System.out.println("Dev App Server Controller stopping app server " + id);
          
          synchronized (this) {
            process = processes.get(id);
            if (process != null)
              process.destroy();
            processes.remove(id);
          }          
          output.writeByte(OK);
          break;
        case KILL:
          output.writeByte(OK);
          kill();
          return;
        case BYE:
          return;
        }
      }
    } catch (IOException e) {
      System.err.println("Dev App Server Controller Error: " + e.getMessage());
    } finally {
      try {
        socket.close();
      } catch (Exception e) {
      }
    }
  }
  
  private boolean isAlive(Process process) {
    // why isn't there a way to tell if a process is still alive?
    try {
      process.exitValue();
    } catch (IllegalThreadStateException e) {
      return true;
    }
    return false;
  }
  
  private synchronized void start(long id, List<String> command, boolean synchronous) throws IOException, InterruptedException {
    ProcessBuilder builder = new ProcessBuilder(command);
    builder.redirectErrorStream(true);
    
    Process process = builder.start();
    processes.put(id, process);
    
    if (synchronous)
      process.wait();
  }
  
  private synchronized void setupLogging(final long id, Socket socket, DataOutputStream output) throws IOException {
    final Process process = processes.get(id);
    if (process == null) {
      output.writeByte(ERR);
      return;
    }    
    
    final DataOutputStream threadOutput = new DataOutputStream(socket.getOutputStream());
    
    Thread outThread = outThreads.get(id);
    if (outThread == null || !outThread.isAlive()) {
      outThread = new Thread() {
        @Override
        public void run() {
          try (Scanner stdOut = new Scanner(process.getInputStream())) {
            while (stdOut.hasNextLine() && !Thread.interrupted()) {
              threadOutput.writeLong(id);
              threadOutput.writeByte(STDOUT);
              threadOutput.writeUTF(stdOut.nextLine());
            }
          } catch (IOException e) {
          }
        }
      };
      outThreads.put(id, outThread);
    }
    
    Thread errThread = errThreads.get(id);
    if (errThread == null || !errThread.isAlive()) {
      errThread = new Thread() {
        @Override
        public void run() {
          try (Scanner stdErr = new Scanner(process.getErrorStream())) {
            while (stdErr.hasNextLine() && !Thread.interrupted()) {
              threadOutput.writeLong(id);
              threadOutput.writeByte(STDERR);
              threadOutput.writeUTF(stdErr.nextLine());
            }
          } catch (IOException e) {
          }
        }
      };
      errThreads.put(id, errThread);
    }
  }
  
  private synchronized void kill() {
    System.out.println("Dev App Server Controller killed");
    this.killed = true;
  }
  
  private static boolean controllerIsAlive(DevAppServerPreferences prefs) {
    try (DevAppServerClient client = new DevAppServerClient(prefs.getPort())) {
      return client.isAlive();
    }
  }
      
  public static void main(String[] args) {
    // check to see if we are already running, otherwise start
    try (DevAppServerPreferences prefs = new DevAppServerPreferences()) {
      if (prefs.getPort() == -1 || !controllerIsAlive(prefs))
      {
        try (DevAppServerController controller = new DevAppServerController(prefs)) {
          controller.run();
        }
      }
    }
  }

}
